package com.start.security.service.Impl;

import cn.hutool.core.util.ReUtil;
import cn.hutool.http.server.HttpServerRequest;
import com.alibaba.fastjson2.JSONArray;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.start.common.exceptions.StartBusException;
import com.start.common.tools.StartRedisTools;
import com.start.common.tools.UUIdTools;
import com.start.common.tools.UrlTools;
import com.start.security.annotations.IgnoreAuth;
import com.start.security.annotations.StartUrlDataSecurity;
import com.start.security.configs.StartDataSecurityProperties;
import com.start.security.configs.StartSecurityProperties;
import com.start.security.data.encryption.sm2.Sm2KeyModel;
import com.start.security.data.encryption.sm2.Sm2Util;
import com.start.security.model.StartDataSecurityModel;
import com.start.security.service.StartSecurityService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;

import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
public class StartSecurityServiceImpl implements StartSecurityService {
    @Autowired
    private StartRedisTools redisTools;


    private final Map<String, Boolean> annoDataSecurityMap = new HashMap<>();


    private final StartSecurityProperties securityProperties;
    private final StartDataSecurityProperties dataSecurityProperties;
    private static final Pattern PATTERN = Pattern.compile("\\{(.*?)}");
    private static final String ASTERISK = StringPool.ASTERISK;
    private final List<String> securityUrls = new ArrayList<>();
    private final List<String> ignoreAuthUrls = new ArrayList<>();

    public StartSecurityServiceImpl(@Autowired @Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping handlerMapping, StartSecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
        this.dataSecurityProperties = securityProperties.getData();
        this.initSecurityUrl(handlerMapping);
    }

    @Override
    public List<String> getIgnoreAuthUrl() {
        return JSONArray.from(ignoreAuthUrls).toList(String.class);
    }

    public List<String> getSecurityUrls() {
        return JSONArray.from(securityUrls).toList(String.class);
    }

    @Override
    public boolean hasDataSecurity(String url) {
        if (StringUtils.isAllBlank(url)) {
            throw new StartBusException("url cannot be must null or empty");
        }
        Set<String> collect = securityUrls.stream().filter(e -> e.contains("*")).collect(Collectors.toSet());
        return UrlTools.antPathMatcher(collect, url, false);
    }


    private void initSecurityUrl(RequestMappingHandlerMapping mapping) {
        securityUrls.clear();
        ignoreAuthUrls.clear();
        ignoreAuthUrls.addAll(securityProperties.getIgnoreUrls());
        Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
        Set<Map.Entry<RequestMappingInfo, HandlerMethod>> entries = map.entrySet();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : entries) {
            handlerIgnoreUrl(entry);
            handlerSecurityDataUrl(entry);
        }
    }

    /**
     * 技术url是否可匿名访问
     *
     * @param entry
     */
    private void handlerIgnoreUrl(Map.Entry<RequestMappingInfo, HandlerMethod> entry) {
        HandlerMethod handlerMethod = entry.getValue();
        Method method = handlerMethod.getMethod();
        boolean hasAuth = hasAuth(method);
        if (hasAuth) {
            return;
        }
        RequestMappingInfo requestMappingInfo = entry.getKey();
        PathPatternsRequestCondition pathPatternsCondition = requestMappingInfo.getPathPatternsCondition();
        if (pathPatternsCondition == null) {
            return;
        }
        mergeRequestUrl(ignoreAuthUrls, entry.getKey());
    }


    private boolean hasAuth(Method method) {
        Class<?> beanClass = method.getClass();
        IgnoreAuth classAnnotation = beanClass.getAnnotation(IgnoreAuth.class);
        IgnoreAuth methodAnnotation = method.getAnnotation(IgnoreAuth.class);

        if (methodAnnotation != null) {
            return !methodAnnotation.value();
        } else if (classAnnotation != null) {
            return !classAnnotation.value();
        }
        return true;

    }

    private void mergeRequestUrl(List<String> urls, RequestMappingInfo mappingInfo) {
        PathPatternsRequestCondition pathPatternsCondition = mappingInfo.getPathPatternsCondition();
        if (pathPatternsCondition == null) {
            return;
        }
        Set<PathPattern> patterns = pathPatternsCondition.getPatterns();
        List<String> collect = patterns.stream().map(pattern -> ReUtil.replaceAll(pattern.getPatternString(), PATTERN, ASTERISK)).toList();
        urls.addAll(collect);
    }

    /**
     * 计算过url是否需要加密传输数据
     *
     * @param entry
     */
    private void handlerSecurityDataUrl(Map.Entry<RequestMappingInfo, HandlerMethod> entry) {
        Boolean security = hasDataSecurity(entry.getValue().getMethod());
        if (Boolean.FALSE.equals(security)) {
            return;
        }
        RequestMappingInfo mappingInfo = entry.getKey();
        mergeRequestUrl(securityUrls, mappingInfo);
    }


    private Boolean hasDataSecurity(Method method) {
        Class<?> declaringClass = method.getDeclaringClass();
        String key = declaringClass.getName() + "." + method.getName();
        if (annoDataSecurityMap.containsKey(key)) {
            return annoDataSecurityMap.get(key);
        }
        final StartUrlDataSecurity clazzIgnoreAuth = AnnotationUtils.findAnnotation(declaringClass, StartUrlDataSecurity.class);
        StartUrlDataSecurity methodAuth = AnnotationUtils.findAnnotation(method, StartUrlDataSecurity.class);

        if (methodAuth == null && clazzIgnoreAuth == null) {
            return null;
        }

        boolean value = Objects.requireNonNullElse(methodAuth, clazzIgnoreAuth).value();
        annoDataSecurityMap.put(key, value);
        return annoDataSecurityMap.get(key);
    }


    @Override
    public StartDataSecurityModel createClientSymmetricKey(String data) {

        if (!dataSecurityProperties.isEnable()) throw new StartBusException("系统未开启数据加密,请检查您的配置文件");
        if (StringUtils.isEmpty(data)) throw new StartBusException("客户端端公钥不能为空");
        String decrypt = Sm2Util.decrypt(data, dataSecurityProperties.getServerPrivateKey());
        if (StringUtils.isEmpty(decrypt)) throw new StartBusException("解密数据失败请填写正确的密钥");
        if (!decrypt.equals(dataSecurityProperties.getServerPrivateKey())) throw new StartBusException("密钥错误");
        String clientServicePrivateKey;
        String clientServicePublicKey;
        String clientId = UUIdTools.createSnowFakeId();
        Sm2KeyModel generator = Sm2Util.generator();
        clientServicePublicKey = generator.getPublicKey();
        clientServicePrivateKey = generator.getPrivateKey();
        if (dataSecurityProperties.getExpire() == null || dataSecurityProperties.getExpire() < 2) {
            redisTools.set(dataSecurityProperties.getRedisKey() + clientId, clientServicePrivateKey);
        } else {
            redisTools.set(dataSecurityProperties.getRedisKey() + clientId, clientServicePrivateKey, dataSecurityProperties.getExpire());
        }
        long expire = redisTools.getExpire(dataSecurityProperties.getRedisKey() + clientId);
        StartDataSecurityModel startDataSecurityModel = new StartDataSecurityModel();
        startDataSecurityModel.setClientId(clientId);
        startDataSecurityModel.setExpire(expire);
        startDataSecurityModel.setClientPublicKey(clientServicePublicKey);
        startDataSecurityModel.setStartDataSecurityUrl(getSecurityUrls());
        return startDataSecurityModel;
    }

    /**
     * 更新client密钥
     *
     * @param clientId
     * @return
     */
    @Override
    public void updateClientDecryptionKey(String clientId) {
        if (StringUtils.isEmpty(clientId)) {
            throw new StartBusException("clientId不能为空");
        }
        if (!redisTools.hasKey(dataSecurityProperties.getRedisKey() + clientId)) {
            return;
        }
        redisTools.expire(dataSecurityProperties.getRedisKey() + clientId, 6 * 60 * 60);
    }

    /**
     * 移除客户端密钥
     *
     * @param clientId
     */
    @Override
    public void deleteClientDecryptionKey(String clientId) {
        if (StringUtils.isEmpty(clientId)) return;
        redisTools.del(dataSecurityProperties.getRedisKey() + clientId);
    }

    @Override
    public void deleteCurrentClientDecryptionKey(HttpServerRequest request) {
        String clientId = request.getHeader(dataSecurityProperties.getHeardClientKey());
        if (StringUtils.isEmpty(clientId)) return;
        deleteClientDecryptionKey(clientId);
    }
}
